/*******************************************************************************
* Copyright (c) 2006-2010 eBay Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*******************************************************************************/
package org.ebayopensource.turmeric.junit.asserts;
import static org.hamcrest.Matchers.*;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import org.junit.Assert;
import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.AbstractJavaEntity;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.model.JavaSource;
import com.thoughtworks.qdox.model.Type;
/**
* Assertions against Java source content.
*/
public class JavaSourceAssert {
/**
* Assert that the java source file is valid, parseable, and is declared as an Interface.
*
* @param java
* the java source file.
* @throws IOException
*/
public static JavaClass assertIsInterface(File java) throws IOException {
JavaDocBuilder builder = new JavaDocBuilder();
JavaSource src = builder.addSource(java);
JavaClass jc = getClassName(src, java);
Assert.assertThat("Failed to parse java source: " + java.getAbsolutePath(), jc, notNullValue());
String msg = String.format("JavaSource: (%s) should be an interface: %s", jc.getName(), java);
Assert.assertThat(msg, jc.isInterface(), is(true));
return jc;
}
/**
* Asserts that the contents of the provided entity contains the expected contents.
* <p>
* The comparison is similar to {@link String#contains(CharSequence)} in so far that only part of the contents has
* to match to be considered a success.
*
* @param entity
* the actual java source entity to compare source against
* @param expectedContents
* the expected contents.
* @throws IOException
*/
public static void assertBodyContains(AbstractJavaEntity entity, CharSequence expectedContents) throws IOException {
String actualSource = codeFormat(entity.getCodeBlock());
String expectedSource = codeFormat(expectedContents);
Assert.assertThat(actualSource, containsString(expectedSource));
}
/**
* Asserts that the contents of two methods are equal.
*
* @param expectedMethod
* the expected java method (signature and content)
* @param actualMethod
* the actual java method (signature and content)
*
* @throws IOException
*/
public static void assertMethodsEqual(JavaMethod expectedMethod, JavaMethod actualMethod) throws IOException {
String expectedSource = codeFormat(expectedMethod);
String actualSource = codeFormat(actualMethod);
Assert.assertEquals(expectedSource, actualSource);
}
/**
* Asserts that the contents of two methods are equal.
*
* @param expectedJava
* the java source of the expected method
* @param actualJava
* the java source of the actual method being tested
* @param signature
* the method signature to lookup in both files and compare against each other.
*
* @throws IOException
*/
public static void assertMethodsEqual(File expectedJava, File actualJava, String signature) throws IOException {
JavaMethod expectedMethod = assertMethodExists(expectedJava, signature);
JavaMethod actualMethod = assertMethodExists(actualJava, signature);
assertMethodsEqual(expectedMethod, actualMethod);
}
/**
* Asserts that the java source file is valid, parseable, and has a constructor with provided signature (no contents
* are checked)
* <p>
* Note: that any fully qualified classname present in the constructor signature will be simplified to just the
* classname.
*
* @param java
* the java source file
* @param signature
* the constructor signature to check for.
* @return the found JavaMethod
* @throws IOException
*/
public static JavaMethod assertConstructorExists(File java, CharSequence signature) throws IOException {
JavaDocBuilder builder = new JavaDocBuilder();
JavaSource src = builder.addSource(java);
JavaClass jc = getClassName(src, java);
Map<String, JavaMethod> sigs = new HashMap<String, JavaMethod>();
for (JavaMethod method : jc.getMethods()) {
if (method.isConstructor()) {
sigs.put(getCompleteMethodSignature(method), method);
}
}
if (!sigs.containsKey(signature)) {
StringBuilder msg = new StringBuilder();
msg.append("Expected constructor signature not found: ");
msg.append(signature);
msg.append("\nConstructors present in java source:");
for (String sig : sigs.keySet()) {
msg.append("\n ").append(sig);
}
Assert.fail(msg.toString());
}
return sigs.get(signature);
}
/**
* Asserts that the java source file is valid, parseable, and has the method with provided signature (no method
* contents are checked)
* <p>
* Note: that any fully qualified classname present in the method signature will be simplified to just the
* classname.
*
* @param java
* the java source file
* @param methodSignature
* the method signature to check for.
* @return the found JavaMethod
* @throws IOException
*/
public static JavaMethod assertMethodExists(File java, CharSequence methodSignature) throws IOException {
JavaDocBuilder builder = new JavaDocBuilder();
JavaSource src = builder.addSource(java);
JavaClass jc = getClassName(src, java);
Map<String, JavaMethod> sigs = new HashMap<String, JavaMethod>();
for (JavaMethod method : jc.getMethods()) {
if (!method.isConstructor()) {
sigs.put(getCompleteMethodSignature(method), method);
}
}
if (!sigs.containsKey(methodSignature)) {
StringBuilder msg = new StringBuilder();
msg.append("Expected method signature not found: ");
msg.append(methodSignature);
msg.append("\nMethods present in java source:");
for (String sig : sigs.keySet()) {
msg.append("\n ").append(sig);
}
Assert.fail(msg.toString());
}
return sigs.get(methodSignature);
}
private static String codeFormat(JavaMethod method) throws IOException {
StringBuilder raw = new StringBuilder();
raw.append(getCompleteMethodSignature(method));
raw.append(" {\n");
raw.append(method.getSourceCode());
raw.append("}");
return codeFormat(raw.toString());
}
/**
* *VERY* basic code formatter, to make code comparison more consistent.
*
* @throws IOException
*/
private static String codeFormat(CharSequence rawcode) throws IOException {
StringBuilder ret = new StringBuilder();
Scanner scanner = new Scanner(new StringReader(rawcode.toString()));
boolean needsIndent = false;
String tok;
String indent = "";
while (scanner.hasNext()) {
tok = scanner.next();
if (tok.equals("{")) {
indent += " ";
ret.append("{\n").append(indent);
}
else if (tok.endsWith(";")) {
if (needsIndent) {
ret.append(indent);
needsIndent = false;
}
ret.append(tok).append("\n");
needsIndent = true;
}
else if (tok.equals("}")) {
indent = indent.substring(4);
ret.append(indent).append("}\n");
}
else {
if (needsIndent) {
ret.append(indent);
needsIndent = false;
}
ret.append(tok).append(' ');
}
}
return ret.toString();
}
private static JavaClass getClassName(JavaSource src, File java) {
JavaClass classes[] = src.getClasses();
String classname = java.getName();
classname = classname.substring(0, classname.length() - ".java".length());
for (JavaClass jc : classes) {
if (jc.getName().equals(classname)) {
return jc;
}
}
return null;
}
private static String getCompleteMethodSignature(JavaMethod method) {
StringBuilder ret = new StringBuilder();
// Accessibility Modifiers
for (String modifier : method.getModifiers()) {
if (modifier.startsWith("p")) {
ret.append(modifier);
ret.append(' ');
}
}
// Non Accessibility Modifiers
for (String modifier : method.getModifiers()) {
if (!modifier.startsWith("p")) {
ret.append(modifier);
ret.append(' ');
}
}
// Returns
if (!method.isConstructor()) {
appendGeneric(ret, method.getReturnType());
ret.append(' ');
}
// Name
ret.append(method.getName());
// Parameters
ret.append('(');
boolean needsDelim = false;
for (JavaParameter parameter : method.getParameters()) {
if (needsDelim) {
ret.append(", ");
}
appendGeneric(ret, parameter.getType());
if (parameter.isVarArgs()) {
ret.append("...");
}
ret.append(' ');
ret.append(parameter.getName());
needsDelim = true;
}
ret.append(')');
// Throws
if (method.getExceptions().length > 0) {
ret.append(" throws ");
needsDelim = false;
for (Type exception : method.getExceptions()) {
if (needsDelim) {
ret.append(", ");
}
appendGeneric(ret, exception);
}
}
return ret.toString();
}
private static void appendGeneric(StringBuilder buf, Type type) {
String fqn = type.getFullyQualifiedName();
int lastDot = fqn.lastIndexOf('.');
int lastDollar = fqn.lastIndexOf('$');
if (lastDot > 0 && lastDollar > 0) {
int split = Math.min(lastDot, lastDollar);
buf.append(fqn.substring(split + 1));
}
else if (lastDot > 0) {
buf.append(fqn.substring(lastDot + 1));
}
else if (lastDollar > 0) {
buf.append(fqn.substring(lastDollar + 1));
}
else {
buf.append(fqn);
}
Type args[] = type.getActualTypeArguments();
if ((args != null) && (args.length > 0)) {
buf.append('<');
boolean needsDelim = false;
for (Type arg : args) {
if (needsDelim) {
buf.append(',');
}
appendGeneric(buf, arg);
needsDelim = true;
}
buf.append('>');
}
for (int dim = 0; dim < type.getDimensions(); dim++) {
buf.append("[]");
}
}
}